昨天看了有趣的useReducer
,今天讓我們看看React當中可以說是最方便的工具:Custom hook(客製化hook)吧。
Custom hook是個必須以"use"作為開頭的函式,在這函式內部我們可以使用各式React hook如useState
、useEffect
,且我們不需回傳jsx
,可以只回傳值就好,以便在整個codebase當中重複利用這個hook(鉤子,所以希望能"掛"在任何元件上)。
BTW,如果你在一個不是以"use"開頭的函式(ex: function sayHello)內部呼叫useState
/useEffect
等hook,你會得到這類錯誤,說明你如果要寫元件,開頭就要大寫,如果是要寫hook,則開頭一定要用use:
React Hook "useState" is called in function "sayHello" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".
讓我們開始今天的內容吧:
直接用個最淺白的例子:"打api",來說明吧!
我們有很高的頻率會在多個元件內打api,依照拿回來的資料去渲染UI,但如果我們都得在每個元件內的useEffect
當中去輸入fetch/axios("randomurl").then.then.catch.finally
(我沒有正確打完,但你應該知道這有多繁複)
想必是件累人的事,就讓我們用custom hook包裝重複的code吧!
我們今天的範例因為是打api,會借助下面這個公開api的資源,如果你在練習時發現api沒能正常運作的話,就只能請你找找其他免費api來試試囉,邏輯都相通的:
https://www.boredapi.com/api/activity/
我們可以繼續在App.tsx
內空白處宣告hook,但更常見的做法是拉到獨立的資料夾內,創一個獨立的檔案來放置code,這給你決定,我一樣放在App.tsx
內
import {useState, useEffect} from 'react'
const useFetchData = (url) => {
const [data, setData] = useState(null)
const [done, setDone] = useState(false)
useEffect(() => {
fetch(url)
.then(resp => resp.json())
.then(data => {
setData(data)
setDone(true)
})
}, [url])
return { data, done }
}
先來個基本版的,我們宣告useFetchData
這個hook,他會根據使用者提供的url去打api,內部使用data陣列來放置api回傳的資料(初始值是null),再用一個flagdone
來標示打完api與否。這邊一定都很簡單,讓我們加上TypeScript吧!
//略過import
//打過一次api就會知道回傳物件的形狀
//可以直接宣告一個interface,晚點使用
interface Activity {
activity: string
type: string
participants: number
price: number
link: string
key: string
accessibility: number
}
//url一定會是字串,所以這邊宣告型別為字串,沒問題
//回傳值的部分,我們知道會是完整的Activity或是null,所以也放進來
//done理所當然是boolean值
const useFetchData = (url: string): { data: Activity | null; done: boolean } => {
//data接受Activity型別或null
const [data, setData] = useState<Activity | null>(null)
const [done, setDone] = useState(false)
useEffect(() => {
fetch(url)
.then((resp) => resp.json())
//這邊也確定拿到的型別一定會是Activity
.then((data: Activity) => {
setData(data)
setDone(true)
})
}, [url])
return { data, done }
}
再讓我們將useFetchData
掛進App
元件吧:
const App = () => {
const { data, done } = useFetchData("https://www.boredapi.com/api/activity/")
//因為done是true,表示一定有資料回來
//所以將data進行非null斷言,加上驚嘆號
return <>{done && <div>{data!.activity}</div>}</>
}
就這樣,你順利將custom hook加上了TypeScript!
不過很明顯的,這個custom hook並沒有這麼好用,只能一直拿來fetch activity,不能拿來fetch其他東西,畢竟我們已經限制了他回傳的型別了。既然這樣,就使用我們前陣子提到的泛型吧!
我們等等就把Activity
型別從hook中拆出來,給他一個泛型型別,這樣我們就能在"呼叫"時,才來決定回傳的型別應該長怎樣!
//這邊我被衝康好久
//在jsx當中如果只將泛型以<T>呈現,他會說你沒有closing tag
//(編輯器以為你在寫jsx)
//所以要在型別T後面加個逗號",",讓編輯器安靜
//並把剛剛有明確寫出Activity的地方都換成T
//T可以隨你命名,跟函式的參數一樣
const useFetchData = <T,>(url: string): { data: T | null; done: boolean } => {
const [data, setData] = useState<T | null>(null)
const [done, setDone] = useState(false)
useEffect(() => {
fetch(url)
.then((resp) => resp.json())
.then((data: T) => {
setData(data)
setDone(true)
})
}, [url])
return { data, done }
}
const App = () => {
//噢耶,我們在呼叫時再決定型別就好
const { data, done } = useFetchData<Activity>(
"https://www.boredapi.com/api/activity/"
)
return <>{done && <div>{data!.activity}</div>}</>
}
就這樣,你不僅學會怎麼為custom hook加上TypeScript,甚至還複習到了泛型,還不錯吧!
特定將型別從Activity
改為T
後,我們甚至不用局限於單一個物件,也能帶入如Dish[]
這樣的型別,呈現出一整個陣列的data,用途更加的廣泛了!
那我們明天見啦,剩幾天而已了!